package online.indigo6a.tryspringcloudsecurity.conf;

import org.springframework.security.crypto.password.PasswordEncoder;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.security.SecureRandom;

/**
 * 实现 <a href="https://github.com/dotnet/AspNetCore/blob/main/src/Identity/Extensions.Core/src/PasswordHasher.cs">...</a>
 */
public class AspIdentityPasswordEncoder implements PasswordEncoder {
    SecureRandom random = new SecureRandom();
    int iterCount = 100000;

    @Override
    public String encode(CharSequence rawPassword) {
        byte[] salt = new byte[16];
        random.nextBytes(salt);
        KeySpec spec = new PBEKeySpec(rawPassword.toString().toCharArray(), salt, (int) 100000, 32*8);
        try {
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
            byte[] encoded = factory.generateSecret(spec).getEncoded();
            byte[] outputBytes = new byte[encoded.length+13+ salt.length];
            outputBytes[0] = 0x01; // format marker
//            https://learn.microsoft.com/zh-cn/dotnet/api/microsoft.aspnetcore.cryptography.keyderivation.keyderivationprf?view=aspnetcore-7.0
            writeNetworkByteOrder(outputBytes, 1, 2);
            writeNetworkByteOrder(outputBytes, 5, iterCount);
            writeNetworkByteOrder(outputBytes, 9, salt.length);
            System.arraycopy(salt, 0, outputBytes, 13, salt.length);
            System.arraycopy(encoded, 0, outputBytes, 13 + salt.length, encoded.length);
            return Base64.getEncoder().encodeToString(outputBytes);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        byte[] bytes = Base64.getDecoder().decode(encodedPassword);
        long prf = readNetworkByteOrder(bytes,1);
        long iterCount = readNetworkByteOrder(bytes,5);
        long saltLength = readNetworkByteOrder(bytes,9);
//        System.out.printf("prf=%s,iterCount=%s,saltLength=%s\n",prf,iterCount,saltLength);
        byte[] salt = new byte[(int) saltLength];
        if (saltLength < 128 / 8)
        {
            System.out.println("salt length error = "+saltLength);
            return false;
        }

        System.arraycopy(bytes,13,salt,0, (int) saltLength);
        System.out.println("salt:"+Arrays.toString(salt));
        int subkeyLength = bytes.length - 13 - salt.length;
        if (subkeyLength < 128 / 8)
        {
            System.out.println("subKey length length error = "+subkeyLength);
            return false;
        }
        byte[] expectedSubkey = new byte[subkeyLength];
        System.arraycopy(bytes, (int) (13+saltLength),expectedSubkey,0,expectedSubkey.length);


        KeySpec spec = new PBEKeySpec(rawPassword.toString().toCharArray(), salt, (int) iterCount, subkeyLength*8);
        try {
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
            byte[] encoded = factory.generateSecret(spec).getEncoded();
            System.out.println("expect:"+Arrays.toString(expectedSubkey));
            System.out.println("encode:"+ Arrays.toString(encoded));
            return Arrays.equals(encoded,expectedSubkey);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

    }
    public static long readNetworkByteOrder(byte[] buffer, int offset) {
        return ((long)(buffer[offset + 0] & 0xff) << 24)
                | ((long)(buffer[offset + 1] & 0xff) << 16)
                | ((long)(buffer[offset + 2] & 0xff) << 8)
                | ((long)(buffer[offset + 3] & 0xff));
    }
    public static void writeNetworkByteOrder(byte[] buffer, int offset, long value) {
        buffer[offset + 0] = (byte) (value >> 24);
        buffer[offset + 1] = (byte) (value >> 16);
        buffer[offset + 2] = (byte) (value >> 8);
        buffer[offset + 3] = (byte) value;
    }


}
